> Tool Use and Function Calling

Budding
planted Jan 8, 2026tended Jan 8, 2026
#ai-agents#tools#function-calling#apis

Tool Use and Function Calling

🌿 Budding note β€” extending agent capabilities through tools.

What is Tool Use?

Tool use enables agents to interact with external systems beyond their training data:

  • APIs: REST, GraphQL, gRPC
  • Databases: SQL, vector stores, key-value
  • File systems: Read/write files
  • Computation: Code execution, calculators
  • External services: Email, Slack, payment processors

Related: AI Agents Fundamentals for agent architectures

Function Calling vs Tool Use

Function calling (API-native):

# LLM returns structured function call
{
    "function": "get_weather",
    "arguments": {
        "location": "Tokyo",
        "unit": "celsius"
    }
}

Tool use (framework abstraction):

# Framework handles execution
tools = [weather_tool, calculator_tool]
agent = Agent(llm=llm, tools=tools)
result = agent.run("What's the weather in Tokyo?")

Claude Tool Use

Claude has native tool use support:

from anthropic import Anthropic

client = Anthropic()

# Define tools
tools = [
    {
        "name": "get_weather",
        "description": "Get current weather for a location",
        "input_schema": {
            "type": "object",
            "properties": {
                "location": {
                    "type": "string",
                    "description": "City name, e.g. Tokyo"
                },
                "unit": {
                    "type": "string",
                    "enum": ["celsius", "fahrenheit"],
                    "description": "Temperature unit"
                }
            },
            "required": ["location"]
        }
    }
]

# Agent loop with tool use
messages = [{"role": "user", "content": "What's the weather in Tokyo?"}]

response = client.messages.create(
    model="claude-sonnet-4-5-20250929",
    max_tokens=1024,
    tools=tools,
    messages=messages
)

# Check if tool was called
if response.stop_reason == "tool_use":
    tool_use_block = next(
        block for block in response.content
        if block.type == "tool_use"
    )

    # Execute tool
    if tool_use_block.name == "get_weather":
        weather_data = get_weather_api(
            location=tool_use_block.input["location"],
            unit=tool_use_block.input.get("unit", "celsius")
        )

        # Continue conversation with result
        messages.append({"role": "assistant", "content": response.content})
        messages.append({
            "role": "user",
            "content": [{
                "type": "tool_result",
                "tool_use_id": tool_use_block.id,
                "content": str(weather_data)
            }]
        })

        # Get final response
        final_response = client.messages.create(
            model="claude-sonnet-4-5-20250929",
            max_tokens=1024,
            tools=tools,
            messages=messages
        )

Related: Claude Agent Patterns for Claude-specific patterns

Tool Definition Best Practices

1. Clear Descriptions

# ❌ Bad: Vague description
{
    "name": "search",
    "description": "Searches stuff",
    "input_schema": {...}
}

# βœ… Good: Specific and actionable
{
    "name": "web_search",
    "description": "Search the web for current information. Use this when you need recent data not in your training (news, weather, stock prices, etc.)",
    "input_schema": {...}
}

2. Detailed Parameter Schemas

{
    "name": "send_email",
    "description": "Send an email to a recipient",
    "input_schema": {
        "type": "object",
        "properties": {
            "to": {
                "type": "string",
                "description": "Recipient email address (must be valid format)"
            },
            "subject": {
                "type": "string",
                "description": "Email subject line (max 100 characters)"
            },
            "body": {
                "type": "string",
                "description": "Email body content (plain text or HTML)"
            },
            "priority": {
                "type": "string",
                "enum": ["low", "normal", "high"],
                "description": "Email priority level",
                "default": "normal"
            }
        },
        "required": ["to", "subject", "body"]
    }
}

3. Include Examples

{
    "name": "database_query",
    "description": "Execute SQL query on the database. Examples: 'SELECT * FROM users WHERE age > 18', 'SELECT COUNT(*) FROM orders WHERE status = \"completed\"'",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {
                "type": "string",
                "description": "SQL SELECT query (read-only)"
            }
        },
        "required": ["query"]
    }
}

Common Tool Patterns

1. Web Search

import requests

def web_search(query: str, num_results: int = 5) -> list[dict]:
    """Search web using DuckDuckGo API"""
    response = requests.get(
        "https://api.duckduckgo.com/",
        params={
            "q": query,
            "format": "json",
            "no_html": 1
        }
    )

    data = response.json()
    return [
        {
            "title": result.get("Title"),
            "snippet": result.get("Text"),
            "url": result.get("FirstURL")
        }
        for result in data.get("RelatedTopics", [])[:num_results]
    ]

# Tool definition
web_search_tool = {
    "name": "web_search",
    "description": "Search the web for current information",
    "input_schema": {
        "type": "object",
        "properties": {
            "query": {"type": "string", "description": "Search query"},
            "num_results": {"type": "integer", "description": "Number of results (default 5)"}
        },
        "required": ["query"]
    }
}

2. Database Access

import sqlite3
from typing import List, Dict, Any

class DatabaseTool:
    def __init__(self, db_path: str):
        self.db_path = db_path

    def query(self, sql: str) -> List[Dict[str, Any]]:
        """Execute read-only SQL query"""
        # Security: Only allow SELECT
        if not sql.strip().upper().startswith("SELECT"):
            raise ValueError("Only SELECT queries allowed")

        conn = sqlite3.connect(self.db_path)
        conn.row_factory = sqlite3.Row
        cursor = conn.cursor()

        try:
            cursor.execute(sql)
            rows = cursor.fetchall()
            return [dict(row) for row in rows]
        finally:
            conn.close()

db_tool = DatabaseTool("./data.db")

# Tool definition
{
    "name": "query_database",
    "description": "Query the company database. Use for customer data, orders, products, etc.",
    "input_schema": {
        "type": "object",
        "properties": {
            "sql": {
                "type": "string",
                "description": "SQL SELECT query (read-only)"
            }
        },
        "required": ["sql"]
    }
}

Related: Agent Security Considerations for database safety

3. File Operations

import os
from pathlib import Path

class FileSystemTool:
    def __init__(self, allowed_dir: str):
        self.allowed_dir = Path(allowed_dir).resolve()

    def read_file(self, path: str) -> str:
        """Read file content"""
        file_path = (self.allowed_dir / path).resolve()

        # Security: Prevent path traversal
        if not str(file_path).startswith(str(self.allowed_dir)):
            raise ValueError("Access denied: Path outside allowed directory")

        return file_path.read_text()

    def write_file(self, path: str, content: str) -> str:
        """Write content to file"""
        file_path = (self.allowed_dir / path).resolve()

        if not str(file_path).startswith(str(self.allowed_dir)):
            raise ValueError("Access denied: Path outside allowed directory")

        file_path.parent.mkdir(parents=True, exist_ok=True)
        file_path.write_text(content)
        return f"Wrote {len(content)} characters to {path}"

    def list_files(self, directory: str = ".") -> List[str]:
        """List files in directory"""
        dir_path = (self.allowed_dir / directory).resolve()

        if not str(dir_path).startswith(str(self.allowed_dir)):
            raise ValueError("Access denied: Path outside allowed directory")

        return [f.name for f in dir_path.iterdir()]

4. Code Execution

import subprocess
import tempfile
from typing import Dict

def execute_python(code: str, timeout: int = 5) -> Dict[str, str]:
    """Execute Python code in sandbox"""
    # Write code to temp file
    with tempfile.NamedTemporaryFile(mode='w', suffix='.py', delete=False) as f:
        f.write(code)
        temp_path = f.name

    try:
        # Execute with timeout
        result = subprocess.run(
            ['python3', temp_path],
            capture_output=True,
            text=True,
            timeout=timeout
        )

        return {
            "stdout": result.stdout,
            "stderr": result.stderr,
            "returncode": result.returncode
        }
    except subprocess.TimeoutExpired:
        return {
            "stdout": "",
            "stderr": f"Execution timed out after {timeout}s",
            "returncode": -1
        }
    finally:
        os.unlink(temp_path)

# Tool definition
{
    "name": "execute_python",
    "description": "Execute Python code and return output. Use for calculations, data processing, etc.",
    "input_schema": {
        "type": "object",
        "properties": {
            "code": {
                "type": "string",
                "description": "Python code to execute"
            }
        },
        "required": ["code"]
    }
}

⚠️ Security warning: See Agent Security Considerations for safe code execution

Multi-Step Tool Use

Agents often need multiple tool calls:

def multi_step_agent(task: str):
    """Agent that chains tool calls"""
    messages = [{"role": "user", "content": task}]
    max_iterations = 10

    for i in range(max_iterations):
        response = client.messages.create(
            model="claude-sonnet-4-5-20250929",
            max_tokens=4096,
            tools=tools,
            messages=messages
        )

        # Add assistant response
        messages.append({"role": "assistant", "content": response.content})

        # Check stop reason
        if response.stop_reason == "end_turn":
            # Task complete
            return extract_final_answer(response)

        elif response.stop_reason == "tool_use":
            # Execute all tool calls in this turn
            tool_results = []

            for block in response.content:
                if block.type == "tool_use":
                    result = execute_tool(block.name, block.input)
                    tool_results.append({
                        "type": "tool_result",
                        "tool_use_id": block.id,
                        "content": str(result)
                    })

            # Add tool results
            messages.append({"role": "user", "content": tool_results})

        elif response.stop_reason == "max_tokens":
            raise Exception("Response truncated - increase max_tokens")

    raise Exception("Max iterations reached without completion")

# Example: Multi-step research task
result = multi_step_agent(
    "Find the current CEO of Microsoft and their age, then calculate how many days they've been CEO"
)
# Agent will:
# 1. web_search("Microsoft CEO")
# 2. web_search("Satya Nadella age")
# 3. calculator("days since February 4, 2014")

Parallel Tool Execution

Some frameworks support parallel tool calls:

async def parallel_tool_execution(tool_calls: list):
    """Execute multiple tools concurrently"""
    import asyncio

    async def execute_single(tool_call):
        tool_name = tool_call["name"]
        tool_args = tool_call["arguments"]

        # Execute tool
        result = await tools[tool_name](**tool_args)

        return {
            "tool_call_id": tool_call["id"],
            "result": result
        }

    # Execute all in parallel
    results = await asyncio.gather(
        *[execute_single(tc) for tc in tool_calls]
    )

    return results

# Example: Research multiple topics simultaneously
tool_calls = [
    {"id": "1", "name": "web_search", "arguments": {"query": "AI agents"}},
    {"id": "2", "name": "web_search", "arguments": {"query": "LangChain"}},
    {"id": "3", "name": "web_search", "arguments": {"query": "AutoGPT"}}
]

results = await parallel_tool_execution(tool_calls)

Error Handling

Tools can fail - handle gracefully:

def safe_tool_execution(tool_name: str, tool_args: dict) -> dict:
    """Execute tool with error handling"""
    try:
        # Validate arguments
        validate_tool_args(tool_name, tool_args)

        # Execute tool
        result = tools[tool_name](**tool_args)

        return {
            "success": True,
            "result": result
        }

    except ValidationError as e:
        return {
            "success": False,
            "error": f"Invalid arguments: {e}",
            "suggestion": "Check the tool schema and try again"
        }

    except TimeoutError:
        return {
            "success": False,
            "error": "Tool execution timed out",
            "suggestion": "Try with different parameters or use a different tool"
        }

    except Exception as e:
        return {
            "success": False,
            "error": f"Tool execution failed: {e}",
            "suggestion": "Check if the tool is available and try again"
        }

# Agent receives structured error
messages.append({
    "role": "user",
    "content": [{
        "type": "tool_result",
        "tool_use_id": tool_id,
        "content": json.dumps(safe_tool_execution(name, args)),
        "is_error": not result["success"]
    }]
})

Tool Discovery

Help agents understand available tools:

def list_available_tools() -> str:
    """Generate tool documentation for agent"""
    doc = "Available tools:\n\n"

    for tool in tools:
        doc += f"**{tool['name']}**\n"
        doc += f"Description: {tool['description']}\n"
        doc += f"Parameters:\n"

        for param, schema in tool['input_schema']['properties'].items():
            required = "required" if param in tool['input_schema'].get('required', []) else "optional"
            doc += f"  - {param} ({schema['type']}, {required}): {schema.get('description', '')}\n"

        doc += "\n"

    return doc

# Add to system prompt
system_prompt = f"""You are a helpful agent. You have access to these tools:

{list_available_tools()}

Use tools when needed to complete tasks."""

Tool Composability

Build complex tools from simple ones:

class ComposableTool:
    def __init__(self, name: str, func: Callable):
        self.name = name
        self.func = func

    def __call__(self, *args, **kwargs):
        return self.func(*args, **kwargs)

    def __or__(self, other):
        """Compose tools with | operator"""
        def composed(*args, **kwargs):
            result1 = self(*args, **kwargs)
            return other(result1)

        return ComposableTool(
            name=f"{self.name}_then_{other.name}",
            func=composed
        )

# Define simple tools
search = ComposableTool("search", lambda q: web_search(q))
summarize = ComposableTool("summarize", lambda text: llm_summarize(text))
translate = ComposableTool("translate", lambda text: llm_translate(text, "es"))

# Compose into pipeline
research_pipeline = search | summarize | translate

# Use composed tool
result = research_pipeline("AI agents")
# Searches β†’ Summarizes β†’ Translates to Spanish

Caching Tool Results

Avoid redundant API calls:

from functools import lru_cache
import hashlib
import json

class CachedTool:
    def __init__(self, func: Callable, ttl_seconds: int = 3600):
        self.func = func
        self.cache = {}
        self.ttl = ttl_seconds

    def __call__(self, **kwargs):
        # Create cache key from arguments
        cache_key = hashlib.md5(
            json.dumps(kwargs, sort_keys=True).encode()
        ).hexdigest()

        # Check cache
        if cache_key in self.cache:
            cached_at, result = self.cache[cache_key]
            if time.time() - cached_at < self.ttl:
                return result

        # Execute and cache
        result = self.func(**kwargs)
        self.cache[cache_key] = (time.time(), result)

        return result

# Wrap expensive tools
web_search = CachedTool(web_search_api, ttl_seconds=1800)  # 30 min cache

Tool Monitoring

Track tool usage and performance:

class MonitoredTool:
    def __init__(self, name: str, func: Callable):
        self.name = name
        self.func = func
        self.call_count = 0
        self.total_duration = 0
        self.errors = 0

    def __call__(self, **kwargs):
        self.call_count += 1
        start = time.time()

        try:
            result = self.func(**kwargs)
            duration = time.time() - start
            self.total_duration += duration

            logger.info(f"Tool {self.name} completed in {duration:.2f}s")
            return result

        except Exception as e:
            self.errors += 1
            logger.error(f"Tool {self.name} failed: {e}")
            raise

    def stats(self) -> dict:
        return {
            "name": self.name,
            "calls": self.call_count,
            "errors": self.errors,
            "avg_duration": self.total_duration / self.call_count if self.call_count > 0 else 0,
            "success_rate": (self.call_count - self.errors) / self.call_count if self.call_count > 0 else 0
        }

Related: Agent Evaluation & Testing

Connection Points

Prerequisites:

Related:

Advanced: